// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:io';

import 'package:pub/src/ignore.dart';
import 'package:test/test.dart';

void main() {
  group('Ignore.ignores', () {
    // just for sanity checking
    test('simple case', () {
      final ig = Ignore(['*.dart']);

      expect(ig.ignores('file.dart'), isTrue);
      expect(ig.ignores('lib/file.dart'), isTrue);
      expect(ig.ignores('README.md'), isFalse);
    });
  });

  group('pub', () {
    void testIgnorePath(
      TestData c,
      String path,
      bool expected,
      bool ignoreCase,
    ) {
      final casing = 'with ignoreCase = $ignoreCase';
      test('${c.name}: Ignore.ignores("$path") == $expected $casing', () {
        var hasWarning = false;
        final pathWithoutSlash =
            path.endsWith('/') ? path.substring(0, path.length - 1) : path;

        Iterable<String> listDir(String dir) {
          // List the next part of path:
          if (dir == pathWithoutSlash) return [];
          final nextSlash = path.indexOf('/', dir == '.' ? 0 : dir.length + 1);
          return [path.substring(0, nextSlash == -1 ? path.length : nextSlash)];
        }

        Ignore? ignoreForDir(String dir) =>
            c.patterns[dir] == null
                ? null
                : Ignore(
                  c.patterns[dir]!,
                  onInvalidPattern: (_, _) => hasWarning = true,
                  ignoreCase: ignoreCase,
                );

        bool isDir(String candidate) =>
            candidate == '.' ||
            path.length > candidate.length && path[candidate.length] == '/';

        final r = Ignore.listFiles(
          beneath: pathWithoutSlash,
          includeDirs: true,
          listDir: listDir,
          ignoreForDir: ignoreForDir,
          isDir: isDir,
        );
        if (expected) {
          expect(
            r,
            isEmpty,
            reason: 'Expected "$path" to be ignored, it was NOT!',
          );
        } else {
          expect(r, [
            pathWithoutSlash,
          ], reason: 'Expected "$path" to NOT be ignored, it was IGNORED!');
        }

        // Also test that the logic of walking the tree works.
        final r2 = Ignore.listFiles(
          includeDirs: true,
          listDir: listDir,
          ignoreForDir: ignoreForDir,
          isDir: isDir,
        );
        if (expected) {
          expect(
            r2,
            isNot(contains(pathWithoutSlash)),
            reason: 'Expected "$path" to be ignored, it was NOT!',
          );
        } else {
          expect(
            r2,
            contains(pathWithoutSlash),
            reason: 'Expected "$path" to NOT be ignored, it was IGNORED!',
          );
        }
        expect(hasWarning, c.hasWarning);
      });
    }

    for (final c in testData) {
      c.paths.forEach((path, expected) {
        final ignoreCase = c.ignoreCase;
        if (ignoreCase == null) {
          testIgnorePath(c, path, expected, false);
          testIgnorePath(c, path, expected, true);
        } else {
          testIgnorePath(c, path, expected, ignoreCase);
        }
      });
    }
  });

  ProcessResult runGit(List<String> args, {String? workingDirectory}) {
    final executable = Platform.isWindows ? 'cmd' : 'git';
    args = Platform.isWindows ? ['/c', 'git', ...args] : args;
    return Process.runSync(
      executable,
      args,
      workingDirectory: workingDirectory,
    );
  }

  group('git', () {
    Directory? tmp;

    setUpAll(() async {
      tmp = await Directory.systemTemp.createTemp('package-ignore-test-');

      final ret = runGit(['init'], workingDirectory: tmp!.path);
      expect(
        ret.exitCode,
        equals(0),
        reason:
            'Running "git init" failed. '
            'StdErr: ${ret.stderr} '
            'StdOut: ${ret.stdout}',
      );
    });

    tearDownAll(() async {
      await tmp!.delete(recursive: true);
      tmp = null;
    });

    tearDown(() async {
      runGit(['clean', '-f', '-d', '-x'], workingDirectory: tmp!.path);
    });

    void testIgnorePath(
      TestData c,
      String path,
      bool expected,
      bool ignoreCase,
    ) {
      final casing = 'with ignoreCase = $ignoreCase';
      final result = expected ? 'IGNORED' : 'NOT ignored';
      test(
        '${c.name}: git check-ignore "$path" is $result $casing',
        () async {
          expect(
            runGit([
              'config',
              '--local',
              'core.ignoreCase',
              ignoreCase.toString(),
            ], workingDirectory: tmp!.path).exitCode,
            anyOf(0, 1),
            reason: 'Running "git config --local core.ignoreCase ..." failed',
          );

          for (final directory in c.patterns.keys) {
            final resolvedDirectory =
                directory == '' ? tmp!.uri : tmp!.uri.resolve('$directory/');
            Directory.fromUri(resolvedDirectory).createSync(recursive: true);
            final gitIgnore = File.fromUri(
              resolvedDirectory.resolve('.gitignore'),
            );
            gitIgnore.writeAsStringSync(
              '${c.patterns[directory]!.join('\n')}\n',
            );
          }
          final process = runGit([
            '-C',
            tmp!.path,
            'check-ignore',
            '--no-index',
            path,
          ], workingDirectory: tmp!.path);
          expect(
            process.exitCode,
            anyOf(0, 1),
            reason: 'Running "git check-ignore" failed',
          );
          final ignored = process.exitCode == 0;
          if (expected != ignored) {
            if (expected) {
              fail('Expected "$path" to be ignored, it was NOT!');
            }
            fail('Expected "$path" to NOT be ignored, it was IGNORED!');
          }
        },
        skip:
            Platform.isMacOS || // System `git` on mac has issues...
            c.skipOnWindows && Platform.isWindows,
      );
    }

    for (final c in testData) {
      c.paths.forEach((path, expected) {
        final ignoreCase = c.ignoreCase;
        if (ignoreCase == null) {
          testIgnorePath(c, path, expected, false);
          testIgnorePath(c, path, expected, true);
        } else {
          testIgnorePath(c, path, expected, ignoreCase);
        }
      });
    }
  });
}

class TestData {
  /// Name of the test case.
  final String name;

  /// Patterns for the test case.
  final Map<String, List<String>> patterns;

  /// Map from path to `true` if ignored by [patterns], and `false` if not
  /// ignored by `patterns`.
  final Map<String, bool> paths;

  final bool hasWarning;

  /// Many of the tests don't play well on windows. Simply skip them.
  final bool skipOnWindows;

  /// Test with `core.ignoreCase` set to `true`, `false` or both (if `null`).
  final bool? ignoreCase;

  TestData(
    this.name,
    this.patterns,
    this.paths, {
    this.hasWarning = false,
    this.skipOnWindows = false,
    this.ignoreCase,
  });

  TestData.single(
    String pattern,
    this.paths, {
    this.hasWarning = false,
    this.skipOnWindows = false,
    this.ignoreCase,
  }) : name = '"${pattern.replaceAll('\n', '\\n')}"',
       patterns = {
         '.': [pattern],
       };
}

final testData = [
  // Simple test case
  TestData(
    'simple',
    {
      '.': ['/.git/', '*.o'],
    },
    {
      '.git/config': true,
      '.git/': true,
      'README.md': false,
      'main.c': false,
      'main.o': true,
    },
  ),
  // Test empty lines
  TestData(
    'empty',
    {
      '.': [''],
    },
    {'README.md': false},
  ),
  // Patterns given in multiple lines with comments
  TestData(
    'multiple lines LF',
    {
      '.': [
        '#comment\n/.git/ \n*.o\n',
        // Using CR CR LF doesn't work
        '#comment\n*.md\r\r\n',
        // Tab is not ignored
        '#comment\nLICENSE\t\n',
        // Trailing comments not allowed
        '#comment\nLICENSE  # ignore license\n',
      ],
    },
    {
      '.git/config': true,
      '.git/': true,
      'README.md': false,
      'LICENSE': false,
      'main.c': false,
      'main.o': true,
    },
  ),
  TestData(
    'multiple lines CR LF',
    {
      '.': [
        '#comment\r\n/.git/ \r\n*.o\r\n',
        // Using CR CR LF doesn't work
        '#comment\r\n*.md\r\r\n',
        // Tab is not ignored
        '#comment\r\nLICENSE\t\r\n',
        // Trailing comments not allowed
        '#comment\r\nLICENSE  # ignore license\r\n',
      ],
    },
    {
      '.git/config': true,
      '.git/': true,
      'README.md': false,
      'LICENSE': false,
      'main.c': false,
      'main.o': true,
    },
  ),
  // Test simple patterns
  TestData.single('file.txt', {
    'file.txt': true,
    'other.txt': false,
    'src/file.txt': true,
    '.obj/file.txt': true,
    'sub/folder/file.txt': true,
  }),
  TestData.single('/file.txt', {
    'file.txt': true,
    'other.txt': false,
    'src/file.txt': false,
    '.obj/file.txt': false,
    'sub/folder/file.txt': false,
  }),
  // Test comments and escaping
  TestData.single('#file.txt', {'file.txt': false, '#file.txt': false}),
  TestData.single(r'\#file.txt', {
    '#file.txt': true,
    'other.txt': false,
    'src/#file.txt': true,
    '.obj/#file.txt': true,
    'sub/folder/#file.txt': true,
  }),
  // Test ! and escaping
  TestData.single('!file.txt', {'file.txt': false, '!file.txt': false}),
  TestData(
    'negation',
    {
      '.': ['f*', '!file.txt'],
    },
    {'file.txt': false, '!file.txt': false, 'filter.txt': true},
  ),
  TestData.single(r'\!file.txt', {
    '!file.txt': true,
    'other.txt': false,
    'src/!file.txt': true,
    '.obj/!file.txt': true,
    'sub/folder/!file.txt': true,
  }),
  // Test trailing spaces and escaping
  TestData.single('file.txt   ', {
    'file.txt': true,
    'other.txt': false,
    'src/file.txt': true,
    '.obj/file.txt': true,
    'sub/folder/file.txt': true,
  }),
  TestData.single(r'file.txt\ \     ', {
    'file.txt  ': true,
    'file.txt': false,
    'other.txt  ': false,
    'src/file.txt  ': true,
    'src/file.txt': false,
    '.obj/file.txt  ': true,
    '.obj/file.txt': false,
    'sub/folder/file.txt  ': true,
    'sub/folder/file.txt': false,
  }),
  // Test ending in a slash or not
  TestData.single('folder/', {
    'file.txt': false,
    'folder': false,
    'folder/': true,
    'folder/file.txt': true,
    'sub/folder/': true,
    'sub/folder': false,
    'sub/file.txt': false,
  }),
  TestData.single('folder.txt/', {
    'file.txt': false,
    'folder.txt': false,
    'folder.txt/': true,
    'folder.txt/file.txt': true,
    'sub/folder.txt/': true,
    'sub/folder.txt': false,
    'sub/file.txt': false,
  }),
  TestData.single('folder', {
    'file.txt': false,
    'folder': true,
    'folder/': true,
    'folder/file.txt': true,
    'sub/folder/': true,
    'sub/folder': true,
    'sub/file.txt': false,
  }),
  TestData.single('folder.txt', {
    'file.txt': false,
    'folder.txt': true,
    'folder.txt/': true,
    'folder.txt/file.txt': true,
    'sub/folder.txt/': true,
    'sub/folder.txt': true,
    'sub/file.txt': false,
  }),
  // Test contains a slash makes it relative root
  TestData.single('/folder/', {
    'file.txt': false,
    'folder': false,
    'folder/': true,
    'folder/file.txt': true,
    'sub/folder/': false,
    'sub/folder': false,
    'sub/file.txt': false,
  }),
  TestData.single('/folder', {
    'file.txt': false,
    'folder': true,
    'folder/': true,
    'folder/file.txt': true,
    'sub/folder/': false,
    'sub/folder': false,
    'sub/file.txt': false,
  }),
  TestData.single('sub/folder/', {
    'file.txt': false,
    'folder': false,
    'folder/': false,
    'folder/file.txt': false,
    'sub/folder/': true,
    'sub/folder/file.txt': true,
    'sub/folder': false,
    'sub/file.txt': false,
  }),
  TestData.single('sub/folder', {
    'file.txt': false,
    'folder': false,
    'folder/': false,
    'folder/file.txt': false,
    'sub/folder/': true,
    'sub/folder/file.txt': true,
    'sub/folder': true,
    'sub/file.txt': false,
  }),
  // Special characters from RegExp that are not special in .gitignore
  for (final c in r'(){}+.^$|'.split('')) ...[
    TestData.single('${c}file.txt', {
      '${c}file.txt': true,
      'file.txt': false,
      'file.txt$c': false,
    }, skipOnWindows: c == '^' || c == '|'),
    TestData.single('file.txt$c', {
      'file.txt$c': true,
      'file.txt': false,
      '${c}file.txt': false,
    }, skipOnWindows: c == '^' || c == '|'),
    TestData.single('fi${c}l)e.txt', {
      'fi${c}l)e.txt': true,
      'f${c}il)e.txt': false,
      'fil)e.txt': false,
    }, skipOnWindows: c == '^' || c == '|'),
    TestData.single('fi${c}l}e.txt', {
      'fi${c}l}e.txt': true,
      'f${c}il}e.txt': false,
      'fil}e.txt': false,
    }, skipOnWindows: c == '^' || c == '|'),
  ],
  // Special characters from RegExp that are also special in .gitignore
  // can be escaped.
  for (final c in r'[]*?\'.split('')) ...[
    TestData.single('\\${c}file.txt', {
      '${c}file.txt': true,
      'file.txt': false,
      'file.txt$c': false,
    }, skipOnWindows: c == r'\'),
    TestData.single('file.txt\\$c', {
      'file.txt$c': true,
      'file.txt': false,
      '${c}file.txt': false,
    }, skipOnWindows: c == r'\'),
    TestData.single('fi\\${c}l)e.txt', {
      'fi${c}l)e.txt': true,
      'f${c}il)e.txt': false,
      'fil)e.txt': false,
    }, skipOnWindows: c == r'\'),
    TestData.single('fi\\${c}l}e.txt', {
      'fi${c}l}e.txt': true,
      'f${c}il}e.txt': false,
      'fil}e.txt': false,
    }, skipOnWindows: c == r'\'),
  ],
  // Special characters from RegExp can always be escaped
  for (final c in r'()[]{}*+?.^$|\'.split('')) ...[
    TestData.single('\\${c}file.txt', {
      '${c}file.txt': true,
      'file.txt': false,
      'file.txt$c': false,
    }, skipOnWindows: c == '^' || c == '|' || c == r'\'),
    TestData.single('file.txt\\$c', {
      'file.txt$c': true,
      'file.txt': false,
      '${c}file.txt': false,
    }, skipOnWindows: c == '^' || c == '|' || c == r'\'),
    TestData.single('file\\$c.txt', {
      'file$c.txt': true,
      'file.txt': false,
      '${c}file.txt': false,
    }, skipOnWindows: c == '^' || c == '|' || c == r'\'),
  ],
  // Ending in backslash (unescaped)
  TestData.single(
    'file.txt\\',
    {
      'file.txt\\': false,
      'file.txt ': false,
      'file.txt\n': false,
      'file.txt': false,
    },
    hasWarning: true,
    skipOnWindows: true,
  ),
  TestData.single(r'file.txt\n', {
    'file.txt\\\n': false,
    'file.txt ': false,
    'file.txt\n': false,
    'file.txt': false,
  }),
  TestData.single('**\\', {
    'file.txt\\\n': false,
    'file.txt ': false,
    'file.txt\n': false,
    'file.txt': false,
  }, hasWarning: true),
  TestData.single('*\\', {
    'file.txt\\\n': false,
    'file.txt ': false,
    'file.txt\n': false,
    'file.txt': false,
  }, hasWarning: true),
  // ? matches anything except /
  TestData.single('?', {'f': true, 'file.txt': false}),
  TestData.single('a?c', {
    'abc': true,
    'abcd': false,
    'a/b': false,
    'ab/': false,
    'folder': false,
    'folder/': false,
    'folder/abc': true,
    'folder/abcd': false,
    'folder/aac': true,
    'abc/': true,
    'abc/file.txt': true,
  }),
  TestData.single('???', {
    'abc': true,
    'abcd': false,
    'a/b': false,
    'ab/': false,
    'folder': false,
    'folder/': false,
    'folder/abc': true,
    'folder/abcd': false,
    'folder/aaa': true,
    'abc/': true,
    'abc/file.txt': true,
  }),
  TestData.single('/???', {
    'abc': true,
    'abcd': false,
    'a/b': false,
    'ab/': false,
    'folder': false,
    'folder/': false,
    'folder/abc': false,
    'folder/abcd': false,
    'folder/aaa': false,
    'abc/': true,
    'abc/file.txt': true,
  }),
  TestData.single('???/', {
    'abc': false,
    'abcd': false,
    'a/b': false,
    'ab/': false,
    'folder': false,
    'folder/': false,
    'folder/abc': false,
    'folder/abcd': false,
    'folder/aaa': false,
    'abc/': true,
    'abc/file.txt': true,
  }),
  TestData.single('???/file.txt', {
    'abc': false,
    'folder': false,
    'folder/': false,
    'folder/abc': false,
    'folder/abcd': false,
    'folder/aaa': false,
    'abc/': false,
    'abc/file.txt': true,
  }),
  // Empty character classes
  TestData.single('a[]c', {
    'abc': false,
    'ac': false,
    'a': false,
    'a[]c': false,
    'c': false,
  }, hasWarning: true),
  TestData.single('a[]', {
    'abc': false,
    'ac': false,
    'a': false,
    'a[]': false,
    'c': false,
  }, hasWarning: true),
  // Invalid character classes
  TestData.single(
    r'a[\]',
    {
      'abc': false,
      'ac': false,
      'a': false,
      'a\\': false,
      'a[]': false,
      'a[': false,
      'a[\\]': false,
      'c': false,
    },
    hasWarning: true,
    skipOnWindows: true,
  ),
  TestData.single(
    r'a[\\\]',
    {
      'abc': false,
      'ac': false,
      'a': false,
      'a[]': false,
      'a[': false,
      'a[\\]': false,
      'c': false,
    },
    hasWarning: true,
    skipOnWindows: true,
  ),
  // Character classes with special characters
  TestData.single(r'a[\\]', {
    'a': false,
    'ab': false,
    'a[]': false,
    'a[': false,
    'a\\': true,
  }, skipOnWindows: true),
  TestData.single(r'a[^b]', {
    'a': false,
    'ab': false,
    'ac': true,
    'a[': true,
    'a\\': true,
  }, skipOnWindows: true),
  TestData.single(r'a[!b]', {
    'a': false,
    'ab': false,
    'ac': true,
    'a[': true,
    'a\\': true,
  }, skipOnWindows: true),
  TestData.single(r'a[[]', {'a': false, 'ab': false, 'a[': true, 'a]': false}),
  TestData.single(r'a[]]', {'a': false, 'ab': false, 'a[': false, 'a]': true}),
  TestData.single(r'a[?]', {'a': false, 'ab': false, 'a??': false, 'a?': true}),
  // Character classes with characters
  TestData.single(r'a[abc]', {
    'a': false,
    'aa': true,
    'ab': true,
    'ac': true,
    'ad': false,
  }),
  // Character classes with ranges
  TestData.single(r'a[a-c]', {
    'a': false,
    'aa': true,
    'ab': true,
    'ac': true,
    'ad': false,
    'ae': false,
  }),
  TestData.single(r'a[a-cf]', {
    'a': false,
    'aa': true,
    'ab': true,
    'ac': true,
    'ad': false,
    'ae': false,
    'af': true,
  }),
  TestData.single(r'a[a-cx-z]', {
    'a': false,
    'aa': true,
    'ab': true,
    'ac': true,
    'ad': false,
    'ae': false,
    'af': false,
    'ax': true,
    'ay': true,
    'az': true,
  }),
  // Character classes with weird-ranges
  TestData.single(r'a[a-c-e]', {
    'a': false,
    'aa': true,
    'ab': true,
    'ac': true,
    'ad': false,
    'af': false,
    'ae': true,
    'a-': true,
  }),
  TestData.single(r'a[--0]', {
    'a': false,
    'a-': true,
    'a.': true,
    'a0': true,
    'a1': false,
  }),
  TestData.single(r'a[+--]', {
    'a': false,
    'a-': true,
    'a+': true,
    'a,': true,
    'a0': false,
  }),
  TestData.single(r'a[a-c]', {
    'a': false,
    'aa': true,
    'ab': true,
    'ac': true,
    'ad': false,
    'a-': false,
  }),
  TestData.single(r'a[\a-c]', {
    'a': false,
    'a\\': false,
    'aa': true,
    'ab': true,
    'ac': true,
    'ad': false,
    'a-': false,
  }),
  TestData.single(r'a[a-\c]', {
    'a': false,
    'a\\': false,
    'aa': true,
    'ab': true,
    'ac': true,
    'ad': false,
    'a-': false,
  }),
  TestData.single(r'a[\a-\c]', {
    'a': false,
    'a\\': false,
    'aa': true,
    'ab': true,
    'ac': true,
    'ad': false,
    'a-': false,
  }),
  TestData.single(r'a[\a\-\c]', {
    'a': false,
    'a\\': false,
    'aa': true,
    'ab': false,
    'a-': true,
    'ac': true,
    'ad': false,
  }),
  // Character classes with dashes
  TestData.single(r'a[-]', {'a-': true, 'a': false}),
  TestData.single(r'a[a-]', {'a-': true, 'aa': true, 'ab': false}),
  TestData.single(r'a[-a]', {'a-': true, 'aa': true, 'ab': false}),
  // TODO: test slashes in character classes
  // Test **, *, [, and [...] cases
  TestData.single('x[a-c-e]', {
    'xa': true,
    'xb': true,
    'xc': true,
    'cd': false,
    'xe': true,
    'x-': true,
  }),
  TestData.single('*', {
    'file.txt': true,
    'other.txt': true,
    'src/file.txt': true,
    '.obj/file.txt': true,
    'sub/folder/file.txt': true,
  }),
  TestData.single('f*', {
    'file.txt': true,
    'otherf.txt': false,
    'src/file.txt': true,
    'folder/other.txt': true,
    'sub/folder/file.txt': true,
    'f': true,
    'f/a': true,
    'a/f': true,
  }),
  TestData.single('*f', {
    'file.txt': false,
    'otherf.txt': false,
    'otherf.paf': true,
    'src/file.txt': false,
    'folder/other.txt': false,
    'sub/folderf/file.txt': true,
  }),
  TestData.single('sub/**/f*', {
    'file.txt': false,
    'otherf.txt': false,
    'other.paf': false,
    'src/file.txt': false,
    'folder/other.txt': false,
    'sub/file.txt': true,
    'sub/f.txt': true,
    'sub/pile.txt': false,
    'sub/other.paf': false,
    'sub/folder/file.txt': true,
    'sub/folder/': true,
    'sub/folder/pile.txt': true,
    'sub/folder/other.paf': true,
    'sub/bolder/': false,
    'sub/bolder/file.txt': true,
    'sub/bolder/pile.txt': false,
    'sub/bolder/other.paf': false,
    'subblob/file.txt': false,
  }),
  TestData.single('sub/', {'sub/': true, 'mop/': false, 'sup': false}),
  TestData.single('sub/**/', {
    'file.txt': false,
    'otherf.txt': false,
    'other.paf': false,
    'src/file.txt': false,
    'folder/other.txt': false,
    'sub/file.txt': false,
    'sub/f.txt': false,
    'sub/pile.txt': false,
    'sub/other.paf': false,
    'sub/folder/': true,
    'sub/sub/folder/': true,
    'sub/folder/file.txt': true,
    'sub/folder/pile.txt': true,
    'sub/folder/other.paf': true,
    'sub/bolder/': true,
    'sub/': false,
    'sub/bolder/file.txt': true,
    'sub/bolder/pile.txt': true,
    'sub/bolder/other.paf': true,
    'subblob/file.txt': false,
  }),
  TestData.single('**/bolder/', {
    'file.txt': false,
    'otherf.txt': false,
    'other.paf': false,
    'src/file.txt': false,
    'sub/folder/bolder': false,
    'sub/folder/other.paf': false,
    'sub/bolder/': true,
    'sub/': false,
    'bolder/': true,
    'bolder': false,
    'sub/bolder/file.txt': true,
    'sub/bolder/pile.txt': true,
    'sub/bolder/other.paf': true,
    'subblob/file.txt': false,
  }),
  TestData(
    'ignores in subfolders only target those',
    {
      '.': ['a.txt'],
      'folder': ['b.txt'],
      'folder/sub': ['c.txt'],
    },
    {
      'a.txt': true,
      'b.txt': false,
      'c.txt': false,
      'folder/a.txt': true,
      'folder/b.txt': true,
      'folder/c.txt': false,
      'folder/sub/a.txt': true,
      'folder/sub/b.txt': true,
      'folder/sub/c.txt': true,
    },
  ),
  TestData(
    'Cannot negate folders that were excluded',
    {
      '.': ['sub/', '!sub/foo.txt'],
    },
    {'sub/a.txt': true, 'sub/foo.txt': true},
  ),
  TestData(
    'Can negate the exclusion of folders',
    {
      '.': ['*.txt', 'sub', '!sub', '!foo.txt'],
    },
    {'sub/a.txt': true, 'sub/foo.txt': false},
  ),
  TestData(
    'Can negate the exclusion of folders 2',
    {
      '.': ['sub/', '*.txt'],
      'folder': ['!sub/', '!foo.txt'],
    },
    {
      'folder/sub/a.txt': true,
      'folder/sub/foo.txt': false,
      'folder/foo.txt': false,
      'folder/a.txt': true,
    },
  ),

  TestData(
    'folder/* does not ignore `folder` itself',
    {
      '.': ['folder/*', '!folder/a.txt'],
    },
    {'folder/a.txt': false, 'folder/b.txt': true},
  ),

  // Case sensitivity
  TestData(
    'simple',
    {
      '.': ['/.git/', '*.o'],
    },
    {
      '.git/config': true,
      '.git/': true,
      'README.md': false,
      'main.c': false,
      'main.o': true,
      'main.O': false,
    },
    ignoreCase: false,
  ),
  // Test simple patterns
  TestData.single('file.txt', {
    'file.TXT': false,
    'file.txT': false,
    'file.txt': true,
    'other.txt': false,
    'src/file.txt': true,
    '.obj/file.txt': true,
    'sub/folder/file.txt': true,
    'src/file.TXT': false,
    '.obj/file.TXT': false,
    'sub/folder/file.TXT': false,
  }, ignoreCase: false),

  // Case insensitivity
  TestData(
    'simple',
    {
      '.': ['/.git/', '*.o'],
    },
    {
      '.git/config': true,
      '.git/': true,
      'README.md': false,
      'main.c': false,
      'main.o': true,
      'main.O': true,
    },
    ignoreCase: true,
  ),
  TestData.single('file.txt', {
    'file.TXT': true,
    'file.txT': true,
    'file.txt': true,
    'other.txt': false,
    'src/file.txt': true,
    '.obj/file.txt': true,
    'sub/folder/file.txt': true,
    'src/file.TXT': true,
    '.obj/file.TXT': true,
    'sub/folder/file.TXT': true,
  }, ignoreCase: true),
];
